//!
//! virtio device definitions.
//! for both the mmio interface, and virtio descriptors.
//! only tested with qemu.
//!
//! the virtio spec:
//! https://docs.oasis-open.org/virtio/virtio/v1.1/virtio-v1.1.pdf
//!

pub const disk = @import("virtio/disk.zig");
pub const Mmio = os.lib.Mmio(Reg, false);

comptime {
    driver.register(&init, "virtio,mmio");
}

fn init(arg: ?*const anyopaque) void {
    const node: *const os.fdt.Node = @ptrCast(@alignCast(arg.?));
    const base = os.fdt.base(node);
    const reg: Mmio = .init(base);
    if (reg.read(.magic) != 0x74726976) @panic("invalid virtio mmio device");
    if (reg.read(.version) != 2) return;

    switch (reg.read(.device_id)) {
        .block => { // a virtio disk
            if (reg.read(.vendor_id) == 0x554d4551) disk.init(reg);
        },
        else => return,
    }
}

// 4.2.2 MMIO Device Register Layout
/// virtio mmio control registers
pub const Reg = enum(usize) {
    magic = 0x000, // 0x74726976
    version = 0x004, // version; should be 2
    status = 0x070, // read/write
    vendor_id = 0x00c, // 0x554d4551

    queue_sel = 0x030, // select queue, write-only
    queue_num_max = 0x034, // max size of current queue, read-only
    queue_num = 0x038, // size of current queue, write-only
    queue_ready = 0x044, // ready bit
    queue_notify = 0x050, // write-only
    interrupt_status = 0x060, // read-only
    interrupt_ack = 0x064, // write-only
    queue_desc_low = 0x080, // physical address for descriptor tablem write-only
    queue_desc_high = 0x084,

    device_id = 0x008, // device type; 1 is net, 2 is disk
    device_features = 0x010,
    device_desc_low = 0x0a0, // physical address for available ring, write only
    device_desc_high = 0x0a4,

    driver_features = 0x020,
    driver_desc_low = 0x090, // physical address for available ring, write-only
    driver_desc_high = 0x094,

    /// Device-specific configuration space
    config_space = 0x100,

    // The driver MUST only use 32 bit wide and aligned reads and writes to access the control registers.
    // For the device-specific configuration space, the driver MUST use 8 bit wide accesses for 8 bit
    // wide fields, 16 bit wide and aligned accesses for 16 bit wide fields and 32 bit wide and aligned
    // accesses for 32 and 64 bit wide fields.
    pub const Int = u32;

    /// status register bits
    pub const Status = packed struct(Int) {
        acknowledge: bool = false,
        driver: bool = false,
        driver_ok: bool = false,
        features_ok: bool = false,
        _: u28 = 0,
    };

    pub const DeviceId = enum(Int) {
        reserved = 0,
        network = 1,
        block = 2,
        console = 3,
        gpu = 16,
        timer = 17,
        input = 18,
        socket = 19,
        crypto = 20,
        memory = 24,
        sound = 25,
        fs = 26,
        watchdog = 35,
        bluetooth = 40,
        gpio = 41,
        camera = 43,
        _,
    };
};

pub fn Device(Feature: type) type {
    return struct {
        /// virtio mmio registers.
        reg: Mmio,

        const Self = @This();
        const Set = std.bit_set.IntegerBitSet(@bitSizeOf(Feature));

        /// 3.1.1 Device Initialization
        pub fn negotiate(self: Self, supported: Feature, unsupported: Feature) !void {
            // reset device
            self.reg.write(.status, .{});

            // set ACKNOWLEDGE and DRIVER status bit
            self.reg.write(.status, .{ .acknowledge = true, .driver = true });

            // negotiate features
            const features: Set = @bitCast(self.reg.read(.device_features));
            const final = features.unionWith(@bitCast(supported)).differenceWith(@bitCast(unsupported));
            self.reg.write(.driver_features, @bitCast(final));

            // tell device that feature negotiation is complete.
            self.reg.set(.status, .{ .features_ok = true });

            // re-read status to ensure features_ok is set.
            if (!self.reg.read(.status).features_ok)
                return error.Unusable;
        }

        pub fn setQueue(self: Self, comptime size: u16, queue: *Queue(size)) void {
            // initialize queue 0.
            self.reg.write(.queue_sel, 0);

            // ensure queue 0 is not in use
            if (self.reg.read(.queue_ready) != 0)
                @panic("virtio disk should not be ready");

            // check maximum queue size.
            const max = self.reg.read(.queue_num_max);
            if (max == 0)
                @panic("virtio disk has no queue 0");
            if (max < size)
                @panic("virtio disk max queue too short");

            // set queue size.
            self.reg.write(.queue_num, size);

            // write to physical addresses.
            self.reg.writeInt(.queue_desc_low, 0, @intFromPtr(&queue.desc));
            self.reg.writeInt(.driver_desc_low, 0, @intFromPtr(&queue.avail));
            self.reg.writeInt(.device_desc_low, 0, @intFromPtr(&queue.used));

            // queue is ready.
            self.reg.write(.queue_ready, 0x1);
        }

        /// tell device we're completely ready.
        pub fn finish(self: Self) void {
            self.reg.set(.status, .{ .driver_ok = true });
        }

        // n is queue number
        pub fn notify(self: Self, n: u16) void {
            self.reg.write(.queue_notify, n);
        }

        /// Acknowledges an interrupt
        pub fn ack(self: Self) void {
            self.reg.write(.interrupt_ack, self.reg.read(.interrupt_status) & 0x3);
        }

        /// read field of configuration space
        pub fn get(self: Self, Config: type, comptime field: []const u8) @FieldType(Config, field) {
            return self.reg.readInt(.config_space, @offsetOf(Config, field), @FieldType(Config, field));
        }

        /// write field of configuration space
        pub fn set(self: Self, Config: type, comptime field: []const u8, value: @FieldType(Config, field)) void {
            return self.reg.writeInt(.config_space, @offsetOf(Config, field), value);
        }
    };
}

/// Split Virtqueues
pub fn Queue(n: u16) type {
    comptime {
        std.debug.assert(std.math.isPowerOfTwo(n));
    }

    return struct {
        /// a set (not a ring) of DMA descriptors, with which the driver tells the device where to read
        /// and write individual disk operations. there are NUM descriptors. most commands consist of a
        /// "chain" (a linked list) of a couple of these descriptors.
        desc: [n]Desc align(16),

        /// a ring in which the driver writes descriptor numbers that the driver would like the device to
        /// process. it only includes the head descriptor of each chain. the ring has NUM elements.
        avail: Avail align(2),

        /// a ring in which the device writes descriptor numbers that the device has finished processing
        /// (just the head of each chain). there are NUM used ring entries.
        used: Used align(4),
        /// we've looked this far in used[2..n]
        used_index: u16,

        const Self = @This();

        pub fn create(gpa: std.mem.Allocator) !*Self {
            const queue = try gpa.create(Self);

            // zero queue memory.
            @memset(&queue.desc, .{});
            queue.avail = .{};
            queue.used = .{};
            queue.used_index = 0;

            return queue;
        }

        pub fn destroy(self: *Self, gpa: std.mem.Allocator) void {
            gpa.destroy(self);
        }

        /// Descriptor Area - used for describing buffers
        pub const Desc = extern struct {
            /// Address (guest-physical)
            addr: u64 = 0,
            /// Length
            len: u32 = 0,
            flags: Flag = .{},
            /// We chain unused descriptors via this, too
            next: u16 = 0,

            /// VRingDesc flags
            pub const Flag = packed struct(u16) {
                /// chained with another descriptor
                next: bool = false,
                /// device writes (vs read)
                write: bool = false,
                /// the buffer contains a list of buffer descriptors
                indirect: bool = false,
                _: u13 = 0,
            };
        };

        /// Driver Area - extra data supplied by driver to the device
        pub const Avail = extern struct {
            flags: u16 = 0, // always zero
            index: u16 = 0, // driver will write ring[index] next
            ring: [n]u16 = @splat(0), // descriptor numbers of chain heads
            unused: u16 = 0,
        };

        /// Device Area - extra data supplied by device to driver
        pub const Used = extern struct {
            flags: u16 = 0, // always zero
            index: u16 = 0, // device increments when it adds a ring[] entry
            ring: [n]Elem = @splat(.{}),
            avail_event: u16 = 0, // only if VIRTIO_F_EVENT_IDX

            /// one entry in the "used" rinf, with which the device tells the driver about completed requests.
            pub const Elem = extern struct {
                id: u32 = 0, // index of start of completed descriptor chain
                len: u32 = 0,
            };
        };
    };
}

const std = @import("std");
const os = @import("../os.zig");
const driver = os.driver;
